const pixelRatio = window.devicePixelRatio;
const regl = createREGL({
  pixelRatio,
  extensions: [
    'ANGLE_instanced_arrays',
    'OES_standard_derivatives',
  ],
  attributes: {
    antialias: true
  }
});
regl._gl.canvas.style.position = 'fixed';

function createNumberCanvas (size) {
  const canvas = document.createElement('canvas');
  const img = document.createElement('img');
  const ctx = canvas.getContext('2d');
  canvas.width = 10 * size;
  canvas.height = size;
  ctx.font = `${size}px monospace`;
  ctx.fillStyle = '#000';
  ctx.fillRect(0, 0, size * 10, size);
  ctx.fillStyle = '#fff';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  for (let i = 0; i < 10; i++) {
    ctx.fillText(i, (i + 0.5) * size, size * 0.5);
  }

  const returnValue = new Promise(resolve => {
    img.onload = () => resolve(img);
  });
  img.src = canvas.toDataURL();
  return returnValue;
}

const createREGLProxy = function (regl, argWrapper) {
  const proxy = args => regl({...args, ...argWrapper(args)});
  Object.assign(proxy, regl);
  return proxy;
}

const reglProxy = createREGLProxy(regl, function (args) {
  if (!args.vert) return {};
  return {
    vert: args.vert.slice(0, args.vert.length - 1) + `
    float theta = (2.0 * pi) * (0.5 + index / 8.428);
      gl_Position.xy += vec2(cos(theta), sin(theta)) * numberOffset * pixelRatio / _resolution;
    }`
  }
});

const state = wrapGUI(State({
  lineConfig: State.Section({
    capResolution: State.Slider(8, {min: 1, max: 32, step: 1}),
    joinResolution: State.Slider(2, {min: 1, max: 32, step: 1}),
    cap: State.Select('round', {options: ['round', 'square', 'none']}),
    join: State.Select('round', {options: ['round', 'miter', 'bevel']}),
    miterLimit: State.Slider(1.7, {min: 1, max: 8, step: 0.01}),
    insertCaps: true
  }, {label: 'line config', expanded: true}),
  geometry: State.Section({
    stretch: State.Slider(0.9, {min: -2, max: 2, step: 0.001}),
    flip: State.Slider(1.0, {min: -1, max: 1, step: 0.001}),
  }, {expanded: true}),
  line: State.Section({
    width: State.Slider(70, {min: 1, max: 100, step: 0.1}),
    opacity: State.Slider(0.8, {min: 0, max: 1, step: 0.01}),
  }, {label: 'line', expanded: false}),
  border: State.Section({
    width: State.Slider(5, {min: 0, max: 10, step: 0.1}),
    opacity: State.Slider(0.65, {min: 0, max: 1, step: 0.01}),
  }, {expanded: false}),
  dash: State.Section({
    length: State.Slider(0.2, {min: 0, max: 2, step: 0.05}),
    opacity: State.Slider(0.3, {min: 0, max: 1, step: 0.01}),
  }, {expanded: false, label: 'dash'}),
  rendering: State.Section({
    wireframeOpacity: State.Slider(0.5, {min: 0, max: 1, step: 0.01}),
    cull: State.Select('none', {options: ['none', 'front', 'back']}),
    depth: false,
    colorInstances: true,
    labelPoints: false
  }, {
    expanded: false
  })
}), {
  containerCSS: `
    position: absolute;
    right: 0;
  `
});
state.$onChange(draw);

function project(p) {
  return [
    (0.5 + 0.5 * (p[0] * Math.pow(state.geometry.stretch, 4.0) * Math.sign(state.geometry.stretch) - 0.2)) * window.innerWidth,
    (0.5 + 0.5 * (p[1] * Math.pow(state.geometry.flip, 4.0) * Math.sign(state.geometry.flip))) * window.innerHeight,
  ];
}

const path = [
  //[-Infinity, -Infinity],
  [-0.9,  0.1],
  [-0.8,  -0.3],
  [-0.7,  -0.4],
  [-0.5, -0.5],
  [-0.4, -0.3],
  [-Infinity, -Infinity],
  [-0.3, -0.1],
  [-0.2, 0.1],
  [-Infinity, -Infinity],
  [-0.1, 0.3],
  [ 0, 0.5],
  [ 0.3,  0.4],
  [ 0.4, -0.5],
  [ 0.7, -0.5],
  [ 0.9, 0.5],
  //[-Infinity, -Infinity],
];
const dist = Array(path.length).fill(0);

function computeCumulativeDistance (dist, path, project) {
  let prevPoint = project(path[0]);
  for (let i = 1; i < path.length; i++) {
    dist[i] = dist[i - 1];
    const point = project(path[i]);
    const l = Math.hypot(point[0] - prevPoint[0], point[1] - prevPoint[1]);
    if (!isNaN(l) && isFinite(l)) dist[i] += l;
    prevPoint = point;
  }
  return dist;
}

const lineData = {
  vertexCount: path.length,
  vertexAttributes: {
    point: regl.buffer(path),
    dist: regl.buffer(dist),
  },
  endpointCount: 2,
  endpointAttributes: {
    point: regl.buffer([path.slice(0, 3), path.slice(-3).reverse()]),
    dist: regl.buffer([dist.slice(0, 3), dist.slice(-3).reverse()]),
  },
};

let numTex = regl.texture([[0, 0, 0, 0]]);
createNumberCanvas(30).then(img => {
  numTex = numTex({data: img, min: 'linear', mag: 'linear'})
  draw();
});

const commandCache = {};
function getDrawLines(config) {
  config = Object.assign({}, {
    primitive: 'triangle strip',
  }, config);

  const {primitive} = config;
  const cacheKey = JSON.stringify(config);

  if (!commandCache[cacheKey]) {
    commandCache[cacheKey] = reglLines(reglProxy, {
      debug: true,
      vert: `
        precision highp float;

        #pragma lines: attribute vec2 point;
        #pragma lines: attribute float dist;
        #pragma lines: position = project(point);
        #pragma lines: width = getWidth();
        #pragma lines: extrapolate varying float dist = getProgress(dist);

        uniform float stretch, flip, lineWidth, borderWidth, numberOffset;
        uniform float pixelRatio;

        float getProgress(float p) { return p; }
        float getPointIndex(float p) { return p; }

        vec4 project (vec2 p) {
          if (p.x <= -1.0) return vec4(0);
          return vec4(p * vec2(pow(abs(stretch), 4.0) * sign(stretch), pow(abs(flip), 4.0) * sign(flip)), 0, 1) - vec4(0.2,0,0,0);
        }

        float getWidth () {
          gl_PointSize = 10.0 * pixelRatio; // Oops; has to be anywhere within main func
          return lineWidth;
        }`,
      frag: `
        #extension GL_OES_standard_derivatives : enable
        precision highp float;

        uniform bool squareCap, useBorder, colorInstances;
        uniform float pixelRatio, dashLength, lineWidth, borderWidth, wireframeOpacity;
        uniform vec4 borderColor, lineColor, dashColor;
        varying float vertexIndex;
        uniform sampler2D numTex;

        varying vec3 lineCoord;
        varying float dist;
        varying float instanceID;
        varying vec2 triStripCoord;
        varying float dir;

        float grid (vec3 parameter, float width, float feather) {
          float w1 = width - feather * 0.5;
          vec3 d = fwidth(parameter);
          vec3 looped = 0.5 - abs(mod(parameter, 1.0) - 0.5);
          vec3 a3 = smoothstep(d * (w1 + feather), d * w1, looped);
          return max(max(a3.x, a3.y), a3.z);
        }

        float linearstep (float a, float b, float x) {
          return clamp((x - a) / (b - a), 0.0, 1.0);
        }

        void main () {
          float sdf = lineWidth * 0.5 * (
            squareCap ? max(abs(lineCoord.x), abs(lineCoord.y)) : length(lineCoord.xy)
          );

          gl_FragColor.a = lineColor.a;

          gl_FragColor.rgb = vec3(0.4, 0.7, 1.0);
          if (colorInstances) {
            if (instanceID < 0.0) {
              gl_FragColor.rgb = vec3(0.8, 0.1, 0.4);
            } else if (floor(mod(instanceID, 2.0) + 0.5) == 1.0) {
              gl_FragColor.rgb = vec3(0.2, 0.3, 0.7);
            }
          } else {
            gl_FragColor.rgb = lineColor.rgb;
          }

#if ${primitive === 'triangle strip' ? 0 : 1}
          bool neg = vertexIndex < 0.0;
          vec2 uv = gl_PointCoord.xy;
          float ones = mod(floor(abs(vertexIndex) + 0.5), 10.0);
          float tens = floor((abs(vertexIndex) + 0.5) / 10.0);
          if (tens == 0.0) uv.x += 0.25;
          if (tens == 0.0 && uv.x < 0.5 || uv.x > 1.0) discard;
          vec2 numCenter = vec2(((uv.x < 0.5 ? tens : ones) + 0.5) / 10.0, 0.5);
          vec2 numRange = vec2(0.5 / 10.0, 0.5);
          vec2 numCoord = vec2(fract(uv.x * 2.0) - 0.5, uv.y * 2.0 - 1.0);
          gl_FragColor = vec4(
            mix(gl_FragColor.rgb, vec3(1), 0.8),
            texture2D(numTex, numCenter + numRange * numCoord).r
          );
#else

          float dl = dashLength;
          if (dashColor.a > 0.0 && dashLength > 0.0) {
            float dashvar = fract(dist / dl) * dl;
            float dash = linearstep(0.0, 1.0, dashvar)
              * linearstep(dl * 0.5 + 1.0 / pixelRatio, dl * 0.5, dashvar);
            //if (lineCoord.z > 0.0) dash = 0.0;
            gl_FragColor.a *= mix(1.0, 1.0 - dashColor.a, dash);
          }

          if (useBorder && borderColor.a > 0.0) {
            float border = linearstep(
              lineWidth * 0.5 - borderWidth - 0.5,
              lineWidth * 0.5 - borderWidth + 0.5,
              sdf
            );

            vec3 borderCol = lineCoord.y > 0.0 ? vec3(1, 0, 0) : vec3(0,0,1);
            gl_FragColor.rgb = mix(gl_FragColor.rgb, borderCol, border * borderColor.a);
            gl_FragColor.a = max(gl_FragColor.a, borderColor.a * border);
          }

          // Draw unit grid lines and a diagonal line using the vertex ID turned into a vec2 varying.
          //
          //   0     2     4     6     8
          //   + --- + --- + --- + --- +
          //   |   / |   / |   / |   / |
          //   | /   | /   | /   | /   |
          //   + --- + --- + --- + --- +
          //   1     3     5     7     9
          //
          if (wireframeOpacity > 0.0) {
            float wire = grid(vec3(triStripCoord, triStripCoord.x + triStripCoord.y), 0.5 * pixelRatio, 2.0 / pixelRatio);
            gl_FragColor = mix(gl_FragColor, vec4(1), wire * wireframeOpacity);
          }
#endif
        }`,
      uniforms: {
        numTex,
        colorInstances: regl.prop('rendering.colorInstances'),
        numberOffset: regl.prop('numberOffset'),
        wireframeOpacity: regl.prop('rendering.wireframeOpacity'),
        useBorder: (ctx, props) => props.border.width > 0,
        lineColor: regl.prop('lineColor'),
        borderColor: regl.prop('borderColor'),
        dashColor: regl.prop('dashColor'),
        squareCap: (ctx, props) => props.cap === 'square',
        stretch: regl.prop('geometry.stretch'),
        flip: regl.prop('geometry.flip'),
        pixelRatio: regl.context('pixelRatio'),
        lineWidth: (ctx, props) => ctx.pixelRatio * props.line.width,
        borderWidth: (ctx, props) => ctx.pixelRatio * props.border.width,
        dashLength: (ctx, props) => props.line.width * props.dash.length * 2.0,
      },
      blend: {
        enable: true,
        func: {
          srcRGB: 'src alpha',
          srcAlpha: 1,
          dstRGB: 'one minus src alpha',
          dstAlpha: 1
        }
      },
      cull: {
        enable: (ctx, props) => props.rendering.cull !== 'none',
        face: (ctx, props) => props.rendering.cull === 'none' ? 'front' : props.rendering.cull
      },
      depth: {
        enable: (ctx, props) => !!props.rendering.depth
      },
      primitive
    });
  }
  return commandCache[cacheKey];
}

function updateBuffers () {
  //lineData.vertexAttributes.xy.subdata(path);
  //lineData.endpointAttributes.xy.subdata([path.slice(0, 3), path.slice(-3).reverse()]);
  lineData.vertexAttributes.dist.subdata(dist);
  lineData.endpointAttributes.dist.subdata([dist.slice(0, 3), dist.slice(-3).reverse()]);
}

function draw () {
  computeCumulativeDistance(dist, path, project);
  updateBuffers();

  regl.poll();
  regl.clear({color: [0.2, 0.2, 0.2, 1], depth: 1});

  getDrawLines({})({
    ...lineData,
    ...state.lineConfig,
    ...state,
    lineColor: [0.3, 0.2, 0.8, state.line.opacity],
    borderColor: [0, 0, 0, state.border.opacity],
    dashColor: [0, 0, 0, state.dash.opacity],
    primitive: 'triangle strip',
    numberOffset: 0,
  });

  if (state.rendering.labelPoints) {
    getDrawLines({primitive: 'points'})({
      ...lineData,
      ...state.lineConfig,
      ...state,
      lineColor: [0, 0, 0, state.line.opacity],
      borderColor: [0, 0, 0, state.border.opacity],
      dashColor: [0, 0, 0, state.dash.opacity],
      primitive: 'points',
      numberOffset: 15
    });
  }
}

computeCumulativeDistance(dist, path, project);
draw();

window.addEventListener('resize', draw);
